package ca.wilkinsonlab.sadi.commandline;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.RandomUtils;
import org.apache.commons.lang.time.StopWatch;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;

import com.hp.hpl.jena.graph.Triple;
import com.hp.hpl.jena.ontology.OntModel;
import com.hp.hpl.jena.ontology.OntModelSpec;
import com.hp.hpl.jena.rdf.model.ModelFactory;

import ca.wilkinsonlab.sadi.share.Config;
import ca.wilkinsonlab.sadi.share.QueryPatternOrderingStrategy;
import ca.wilkinsonlab.sadi.share.QueryPlanEnumerator;
import ca.wilkinsonlab.sadi.share.SHAREKnowledgeBase;
import ca.wilkinsonlab.sadi.share.SHAREQueryClient;
import ca.wilkinsonlab.sadi.share.UnresolvableQueryException;
import ca.wilkinsonlab.sadi.stats.PredicateStatsDB;
import ca.wilkinsonlab.sadi.utils.SPARQLStringUtils;

public class SHAREClient
{
	public final static Logger log = Logger.getLogger(SHAREClient.class);

	static {
		/* by default, display only ERROR-level log messages, and do that on STDERR */
		LogManager.resetConfiguration();
		PropertyConfigurator.configure(SHAREClient.class.getResource("default.log4j.properties"));		
	}
	
	public final static int EXIT_CODE_SUCCESS = 0;
	public final static int EXIT_CODE_NO_RESULTS = 1;
	public final static int EXIT_CODE_FAILURE = 2;
	
	public final static String OPTIMIZER_CONFIG_KEY = "share.useAdaptiveQueryPlanning";

	public final static String RECORD_STATS_CONFIG_KEY = "share.recordQueryStats";

	private static final String USAGE = "USAGE: java -jar share.jar [options] < sparql.query.txt";
	
	public static class CommandLineOptions 
	{
		@Option(name="-h", aliases={"--help"}, usage="show usage message")
		boolean help = false;
		
		/* options for running query */

		@Option(name="-q", metaVar="<FILENAME>", aliases={"--query-file"}, usage="file containing the input SPARQL query (otherwise STDIN)")
		String queryFilename = null;

		@Option(name="-n", aliases={"--no-reordering"}, usage="do not reorder the input query, even to make the query resolvable")
		boolean bypassQueryReordering = false;

		@Option(name="-N", aliases={"--no-reasoning"}, usage="disable OWL reasoning when answering the input query")
		boolean noReasoning = false;
		
		@Option(name="-O", aliases={"--optimize"}, usage="enable adaptive query optimization (order of triple patterns is decided as query runs)")
		boolean optimize = false;
		
		/* options output files generated by query */
		
		@Option(name="-l", metaVar="<FILENAME>", aliases={"--log-file"}, usage="output log messages to this file")
		String logFilename = null;
		
		@Option(name="-o", metaVar="<FILENAME>", aliases={"--output-file"}, usage="output query results/enumerations to this file (otherwise STDOUT)")
		String outputFilename = null;
		
		/* stats related options */
		
		@Option(name="-R", aliases={"--record-stats"}, usage="record statistics during query execution. " +
				"This requires write access to the statsdb, so you will probably also have to specify values for the -e, -u, and -p switches" +
				"(endpoint URL, username, password)")
		boolean recordQueryStats = false;
		
		@Option(name="-e", metaVar="<ENDPOINT_URL>", aliases={"--stats-endpoint"}, usage="the SPARQL endpoint URL for reading/writing stats")
		String statsEndpointURL = Config.getConfiguration().subset(PredicateStatsDB.ROOT_CONFIG_KEY).getString(PredicateStatsDB.ENDPOINT_URL_CONFIG_KEY);
		
		@Option(name="-u", metaVar="<USERNAME>", aliases={"--stats-username"}, usage="username for stats endpoint")
		String statsUsername = null;
		
		@Option(name="-p", metaVar="<PASSWORD>", aliases={"--stats-password"}, usage="password for stats endpoint")
		String statsPassword = null;
		
		@Option(name="-s", metaVar="<URI>", aliases={"--samples-graph"}, usage="the namede graph in the statsdb that contains predicate stats from queries")
		String statsSamplesGraph = PredicateStatsDB.DEFAULT_SAMPLES_GRAPH;
		
		@Option(name="-S", metaVar="<URI>", aliases={"--summary-stats-graph"}, usage="the name graph in the statsdb that contains computed summary stats for predicates")
		String statsSummaryGraph = PredicateStatsDB.DEFAULT_STATS_GRAPH;
		
		/* options for enumerating query plans */
		
		@Option(name="-r", aliases={"--random-ordering"}, usage="randomly reorder the triple patterns in the input query")
		public void setRandomOrdering(boolean unused) { this.randomOrdering = true; this.enumerateOrderings = false; } 
		boolean randomOrdering = false;

		@Option(name="-E", aliases={"--enumerate-orderings"}, usage="enumerate all SHARE-resolvable reorderings of the input query")
		public void setEnumerateOrderings(boolean unused) {this.enumerateOrderings = true; this.randomOrdering = false; }
		boolean enumerateOrderings = false;
		
	}
		
	protected static class DoNothingQueryOrderingStrategy implements QueryPatternOrderingStrategy 
	{
		@Override
		public List<Triple> orderPatterns(List<Triple> patterns) throws UnresolvableQueryException {
			return patterns;
		}
	}
	
	protected static void setLogfile(String filename) 
	{
		if(filename.equals("-")) {

			org.apache.log4j.LogManager.resetConfiguration();
			PropertyConfigurator.configure(SHAREClient.class.getResource("log.to.stdout.log4j.properties"));
			
		} else {

			System.setProperty("user.log.file", filename);
			org.apache.log4j.LogManager.resetConfiguration();
			PropertyConfigurator.configure(SHAREClient.class.getResource("log.to.file.log4j.properties"));
		
		}
	}
	
	protected static void close(Writer writer)
	{
		if(writer != null) {
			try {
				writer.close();
			} catch(IOException e) {
				System.err.println("warning: error closing file: " + e.getMessage());
			}
		}
	}
	
	public static void main(String[] args) 
	{
		
		CommandLineOptions options = new CommandLineOptions();
		CmdLineParser cmdLineParser = new CmdLineParser(options);
		
		Writer outputFile = null;
		
		try {

			cmdLineParser.parseArgument(args);
			
			if(options.help) {
				
				System.err.println(USAGE);
				cmdLineParser.printUsage(System.err);
				System.exit(EXIT_CODE_SUCCESS);
				
			}
			
			/*
			 * Setup logging, if the user has specified a target file.
			 */
			
			if(options.logFilename != null) {
				setLogfile(options.logFilename);
			}

			/*
			 * Setup stats related options.
			 * 
			 * The statsDB is a singleton that is created on first access, and it initializes itself
			 * using values from the configuration.
			 */
			
			Configuration statsConfig = Config.getConfiguration().subset(PredicateStatsDB.ROOT_CONFIG_KEY);
			
			statsConfig.setProperty(PredicateStatsDB.ENDPOINT_URL_CONFIG_KEY, options.statsEndpointURL);
			statsConfig.setProperty(PredicateStatsDB.USERNAME_CONFIG_KEY, options.statsUsername);
			statsConfig.setProperty(PredicateStatsDB.PASSWORD_CONFIG_KEY, options.statsPassword);
			statsConfig.setProperty(PredicateStatsDB.SAMPLES_GRAPH_CONFIG_KEY, options.statsSamplesGraph);
			statsConfig.setProperty(PredicateStatsDB.STATS_GRAPH_CONFIG_KEY, options.statsSummaryGraph);
			
			/*
			 * Open the output file (for the query results), or use STDOUT 
			 * if nothing was specified.
			 */
			
			try {
				
				if(options.outputFilename != null) {
					outputFile = new BufferedWriter(new FileWriter(options.outputFilename));
				} else {
					outputFile = new PrintWriter(System.out);
				}

			} catch(IOException e) {
				
				System.err.println("error opening output file: " + e.getMessage());
				System.exit(EXIT_CODE_FAILURE);
				
			}
			
			/* 
			 * Read in the input query.  If no filename has been given with
			 * the -q switch, read the query from standard input.
			 */
			
			String inputQuery = null;
			
			try {
			
				if(options.queryFilename != null) {
					/* 
					 * the funny looking conversion from URI() => URL() here
					 * is recommended by the javadoc for File.toURL() 
					 */
					inputQuery = SPARQLStringUtils.readFully(new File(options.queryFilename).toURI().toURL());
				} else {
					inputQuery = SPARQLStringUtils.readFully(System.in);
				}

				inputQuery = StringUtils.strip(inputQuery);
			
				if(inputQuery.length() == 0) {
					throw new CmdLineException("input query file is empty");
				}

			} catch(IOException e) {
			
				System.err.println("error reading input query: " + e.getMessage());
				System.exit(EXIT_CODE_FAILURE);
		
			}
			
			/*
			 * These options print alternate orderings of the input query
			 * to output file, rather than actually running the query. 
			 */
			
			if(options.randomOrdering || options.enumerateOrderings) {
				
				SHAREKnowledgeBase kb = new SHAREKnowledgeBase(ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM_MICRO_RULE_INF), true);

				QueryPlanEnumerator enumerator = new QueryPlanEnumerator(kb);

				List<String> queryPlans = new ArrayList<String>(enumerator.getAllResolvableQueryPlans(inputQuery));

				try {

					if(options.randomOrdering) {

						int index = RandomUtils.nextInt(queryPlans.size());
						outputFile.write(queryPlans.get(index));

					} else {

						for(String queryPlan : queryPlans) {
							outputFile.write(queryPlan + "\n\n");
						}

					}

					outputFile.flush();
				
				} catch(IOException e) {
					
					close(outputFile);
					System.err.println("error writing results to output file: " + e.getMessage());
					System.exit(EXIT_CODE_FAILURE);
					
				}
				
				close(outputFile);
				System.exit(EXIT_CODE_SUCCESS);
			
			} 
			
			/* Run the input query */
			
			SHAREKnowledgeBase kb;
			OntModel reasoningModel;
			
			if(options.noReasoning) {
				reasoningModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_DL_MEM);
				// emulate reasoning by explicitly storing triples implied by inverse and 
				// equivalent property relationships
				Config.getConfiguration().subset(SHAREKnowledgeBase.ROOT_CONFIG_KEY).setProperty(SHAREKnowledgeBase.STORE_INFERRED_TRIPLES_CONFIG_KEY, true);
			} else {
				reasoningModel = ModelFactory.createOntologyModel(OntModelSpec.OWL_MEM_MICRO_RULE_INF);
			}
			
			if(options.bypassQueryReordering) {
				kb = new SHAREKnowledgeBase(reasoningModel, true) {
					@Override
					public void executeQuery(String query) {
						executeQuery(query, new DoNothingQueryOrderingStrategy());
					}
				};
			} else {
				kb = new SHAREKnowledgeBase(reasoningModel, true);
			}

			/* Turn on optimization if requested */
			
			kb.setUseAdaptiveQueryPlanning(options.optimize);

			/* Set statsDB parameters */

			kb.setRecordQueryStats(options.recordQueryStats);
			
			if(options.recordQueryStats) {

				if(options.statsEndpointURL == null || options.statsUsername == null ||	options.statsPassword == null) {
					throw new CmdLineException("if you request recording of stats (-s), you must also specify an endpoint (-e), a username (-u), and a password (-p)");
				}
			
			}
						
			SHAREQueryClient queryClient = new SHAREQueryClient(kb);

			System.err.println("Running query (this could take some time)...");
			
			StopWatch stopWatch = new StopWatch();
			stopWatch.start();
			List<Map<String, String>> results = queryClient.synchronousQuery(inputQuery);
			stopWatch.stop();

			System.err.println(String.format("query finished in %d milliseconds", stopWatch.getTime()));

			/*
			 * Write the query results to the output file.
			 */

			try {

				for(Map<String, String> binding : results) {
					outputFile.write(binding.toString() + "\n");
				}
				
				outputFile.flush();
				close(outputFile);
				
			} catch(IOException e) {
				
				close(outputFile);
				System.err.println("error writing results to output file: " + e.getMessage());
				System.exit(EXIT_CODE_FAILURE);

			} 

			System.exit((results.size() > 0) ? EXIT_CODE_SUCCESS : EXIT_CODE_NO_RESULTS);

		} catch (CmdLineException e) {
		
			System.err.println(e.getMessage());
			System.err.println(USAGE);
			cmdLineParser.printUsage(System.err);
			System.exit(EXIT_CODE_FAILURE);
			
		} finally {

			close(outputFile);
		
		}

	}
	
}
